<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>FunASR 实时转录客户端</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background: white;
            border-radius: 10px;
            padding: 30px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
        }
        .status {
            padding: 15px;
            margin: 10px 0;
            border-radius: 5px;
            font-weight: bold;
        }
        .status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        .status.warning { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; }
        .status.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
        .btn {
            padding: 12px 24px;
            margin: 5px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            font-weight: bold;
        }
        .btn-primary { background: #007bff; color: white; }
        .btn-success { background: #28a745; color: white; }
        .btn-danger { background: #dc3545; color: white; }
        .btn:disabled { background: #6c757d; cursor: not-allowed; }
        .log {
            background: #2d3748;
            color: #e2e8f0;
            padding: 15px;
            border-radius: 5px;
            font-family: monospace;
            height: 200px;
            overflow-y: auto;
            margin-top: 20px;
        }
        .controls {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            margin: 20px 0;
        }
        .transcription-panel {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            margin: 20px 0;
        }
        .result-box {
            background: #f8f9fa;
            padding: 15px;
            border-radius: 5px;
            min-height: 150px;
            border: 1px solid #dee2e6;
        }
        .result-box h4 {
            margin-top: 0;
            color: #495057;
        }
        .result-text {
            line-height: 1.6;
            max-height: 400px;
            overflow-y: auto;
        }
        .chunk-text {
            margin: 5px 0;
            padding: 8px;
            background: white;
            border-radius: 3px;
            border-left: 3px solid #007bff;
        }
        .complete-text {
            font-size: 16px;
            line-height: 1.8;
            color: #212529;
        }
        .stats {
            display: flex;
            justify-content: space-between;
            margin: 10px 0;
            font-size: 12px;
            color: #6c757d;
        }
        @media (max-width: 800px) {
            .controls, .transcription-panel { grid-template-columns: 1fr; }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🎤 FunASR 实时转录客户端</h1>
        <p>基于官方FunASR流式模型的WebSocket实时语音转录</p>
        
        <div id="browserStatus" class="status info">正在检查浏览器兼容性...</div>
        
        <div class="controls">
            <div>
                <h3>连接设置</h3>
                <label>服务器地址:</label>
                <input type="text" id="serverUrl" value="ws://10.199.228.43:8009" style="width: 100%; padding: 8px; margin: 5px 0;">
                
                <label>音频源:</label>
                <select id="audioSource" style="width: 100%; padding: 8px; margin: 5px 0;">
                    <option value="microphone">麦克风</option>
                    <option value="desktop">系统声音</option>
                </select>
                
                <div style="margin-top: 10px;">
                    <button class="btn btn-primary" onclick="testBrowser()">检查浏览器</button>
                    <button class="btn btn-primary" onclick="testAudio()">测试音频</button>
                </div>
            </div>
            
            <div>
                <h3>控制操作</h3>
                <div id="connectionStatus" class="status warning">未连接</div>
                
                <div style="margin-top: 10px;">
                    <button class="btn btn-success" id="connectBtn" onclick="connect()">连接服务器</button>
                    <button class="btn btn-danger" id="disconnectBtn" onclick="disconnect()" disabled>断开连接</button>
                </div>
                
                <div style="margin-top: 10px;">
                    <button class="btn btn-success" id="startBtn" onclick="startRecording()" disabled>开始录音</button>
                    <button class="btn btn-danger" id="stopBtn" onclick="stopRecording()" disabled>停止录音</button>
                    <button class="btn btn-primary" onclick="clearResults()">清空结果</button>
                </div>
            </div>
        </div>
        
        <div class="transcription-panel">
            <div class="result-box">
                <h4>实时转录结果</h4>
                <div class="stats">
                    <span>块数: <span id="chunkCount">0</span></span>
                    <span>句子数: <span id="sentenceCount">0</span></span>
                    <span>置信度: <span id="avgConfidence">0%</span></span>
                </div>
                <div id="realTimeResults" class="result-text">等待转录结果...</div>
            </div>
            
            <div class="result-box">
                <h4>完整转录文本</h4>
                <div class="stats">
                    <span>字符数: <span id="charCount">0</span></span>
                    <span>标点处理: <span id="punctuationStatus">未处理</span></span>
                </div>
                <div id="completeText" class="result-text complete-text">等待转录结果...</div>
            </div>
        </div>
        
        <div class="log" id="logContainer"></div>
    </div>

    <script>
        let socket = null;
        let mediaStream = null;
        let audioContext = null;
        let processor = null;
        let isRecording = false;
        let isConnected = false;
        let sessionId = null;
        
        // 统计数据
        let totalChunks = 0;
        let totalSentences = 0;
        let confidenceSum = 0;
        let confidenceCount = 0;

        function log(message, type = 'info') {
            const timestamp = new Date().toLocaleTimeString();
            const logContainer = document.getElementById('logContainer');
            const colors = {
                info: '#60a5fa',
                error: '#f87171', 
                warning: '#fbbf24',
                success: '#34d399'
            };
            
            logContainer.innerHTML += `<div style="color: ${colors[type] || colors.info}">[${timestamp}] ${message}</div>`;
            logContainer.scrollTop = logContainer.scrollHeight;
            console.log(`[${type.toUpperCase()}] ${message}`);
        }

        function updateStatus(elementId, message, type) {
            const element = document.getElementById(elementId);
            element.className = `status ${type}`;
            element.textContent = message;
        }

        function updateStats(chunkCount, sentenceCount, confidence) {
            document.getElementById('chunkCount').textContent = chunkCount || totalChunks;
            document.getElementById('sentenceCount').textContent = sentenceCount || totalSentences;
            
            if (confidence !== undefined) {
                confidenceSum += confidence;
                confidenceCount++;
                const avgConfidence = (confidenceSum / confidenceCount * 100).toFixed(1);
                document.getElementById('avgConfidence').textContent = avgConfidence + '%';
            }
        }

        function clearResults() {
            document.getElementById('realTimeResults').innerHTML = '等待转录结果...';
            document.getElementById('completeText').innerHTML = '等待转录结果...';
            document.getElementById('charCount').textContent = '0';
            document.getElementById('punctuationStatus').textContent = '未处理';
            
            totalChunks = 0;
            totalSentences = 0;
            confidenceSum = 0;
            confidenceCount = 0;
            updateStats();
            
            log('清空转录结果', 'info');
        }

        function testBrowser() {
            log('开始浏览器兼容性检查...', 'info');
            const issues = [];
            
            // 检查基本API
            if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
                issues.push('不支持MediaDevices API');
            }
            
            if (!window.AudioContext && !window.webkitAudioContext) {
                issues.push('不支持Web Audio API');
            }
            
            if (!window.WebSocket) {
                issues.push('不支持WebSocket');
            }
            
            // 检查安全上下文
            const isSecure = location.protocol === 'https:' || 
                           location.protocol === 'file:' ||
                           location.hostname === 'localhost' || 
                           location.hostname === '127.0.0.1' ||
                           location.hostname.startsWith('192.168.') ||
                           location.hostname.startsWith('10.');
            
            if (!isSecure) {
                issues.push('非安全上下文（建议使用HTTPS）');
            }
            
            if (issues.length === 0) {
                updateStatus('browserStatus', '✅ 浏览器兼容性检查通过', 'success');
                log('浏览器兼容性检查通过', 'success');
            } else {
                updateStatus('browserStatus', `❌ 发现问题: ${issues.join(', ')}`, 'error');
                log(`浏览器兼容性问题: ${issues.join(', ')}`, 'error');
            }
        }

        async function testAudio() {
            log('开始音频功能测试...', 'info');
            
            try {
                // 测试音频权限
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
                log('✅ 音频权限获取成功', 'success');
                
                // 测试音频上下文
                const AudioContextClass = window.AudioContext || window.webkitAudioContext;
                const audioCtx = new AudioContextClass();
                log('✅ 音频上下文创建成功', 'success');
                
                // 清理
                stream.getTracks().forEach(track => track.stop());
                audioCtx.close();
                
                log('音频功能测试完成', 'success');
                
            } catch (error) {
                log(`❌ 音频测试失败: ${error.message}`, 'error');
                
                if (error.name === 'NotAllowedError') {
                    log('解决方案: 请在浏览器中允许音频权限', 'warning');
                } else if (error.name === 'NotSupportedError') {
                    log('解决方案: 请使用HTTPS或localhost访问', 'warning');
                }
            }
        }

        async function connect() {
            try {
                const url = document.getElementById('serverUrl').value;
                log(`连接到服务器: ${url}`, 'info');
                
                socket = new WebSocket(url);
                
                socket.onopen = () => {
                    isConnected = true;
                    updateStatus('connectionStatus', '✅ 已连接', 'success');
                    log('WebSocket连接建立', 'success');
                    
                    document.getElementById('connectBtn').disabled = true;
                    document.getElementById('disconnectBtn').disabled = false;
                    document.getElementById('startBtn').disabled = false;
                };
                
                socket.onmessage = (event) => {
                    try {
                        const data = JSON.parse(event.data);
                        handleMessage(data);
                    } catch (e) {
                        log(`消息解析失败: ${e.message}`, 'error');
                    }
                };
                
                socket.onclose = () => {
                    isConnected = false;
                    updateStatus('connectionStatus', '❌ 连接断开', 'error');
                    log('WebSocket连接关闭', 'warning');
                    resetUI();
                };
                
                socket.onerror = (error) => {
                    log(`WebSocket错误: ${error}`, 'error');
                };
                
            } catch (error) {
                log(`连接失败: ${error.message}`, 'error');
            }
        }

        function disconnect() {
            if (socket) {
                socket.close();
            }
            if (isRecording) {
                stopRecording();
            }
            resetUI();
        }

        function resetUI() {
            document.getElementById('connectBtn').disabled = false;
            document.getElementById('disconnectBtn').disabled = true;
            document.getElementById('startBtn').disabled = true;
            document.getElementById('stopBtn').disabled = true;
        }

        async function startRecording() {
            try {
                log('开始录音...', 'info');
                
                const audioSource = document.getElementById('audioSource').value;
                let constraints;
                
                if (audioSource === 'desktop') {
                    constraints = { audio: true, video: false };
                    mediaStream = await navigator.mediaDevices.getDisplayMedia(constraints);
                } else {
                    constraints = { 
                        audio: { 
                            sampleRate: 16000,
                            channelCount: 1,
                            echoCancellation: true
                        } 
                    };
                    mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
                }
                
                // 创建音频处理
                const AudioContextClass = window.AudioContext || window.webkitAudioContext;
                audioContext = new AudioContextClass({ sampleRate: 16000 });
                
                if (audioContext.state === 'suspended') {
                    await audioContext.resume();
                }
                
                const source = audioContext.createMediaStreamSource(mediaStream);
                
                // 使用ScriptProcessor（更兼容）
                processor = audioContext.createScriptProcessor(4096, 1, 1);
                processor.onaudioprocess = (event) => {
                    if (isRecording && socket && socket.readyState === WebSocket.OPEN) {
                        const inputData = event.inputBuffer.getChannelData(0);
                        sendAudioData(inputData);
                    }
                };
                
                source.connect(processor);
                processor.connect(audioContext.destination);
                
                // 发送开始消息
                sessionId = `session_${Date.now()}`;
                sendMessage({
                    type: 'start',
                    session_id: sessionId
                });
                
                isRecording = true;
                document.getElementById('startBtn').disabled = true;
                document.getElementById('stopBtn').disabled = false;
                
                log('录音已开始', 'success');
                clearResults();
                
            } catch (error) {
                log(`录音启动失败: ${error.message}`, 'error');
                
                if (error.name === 'NotAllowedError') {
                    log('请允许浏览器访问音频设备', 'warning');
                } else if (error.name === 'NotSupportedError') {
                    log('请使用HTTPS访问或更新浏览器', 'warning');
                }
            }
        }

        function stopRecording() {
            try {
                isRecording = false;
                
                if (mediaStream) {
                    mediaStream.getTracks().forEach(track => track.stop());
                    mediaStream = null;
                }
                
                if (processor) {
                    processor.disconnect();
                    processor = null;
                }
                
                if (audioContext) {
                    audioContext.close();
                    audioContext = null;
                }
                
                sendMessage({ type: 'stop' });
                
                document.getElementById('startBtn').disabled = false;
                document.getElementById('stopBtn').disabled = true;
                
                log('录音已停止', 'info');
                
            } catch (error) {
                log(`停止录音失败: ${error.message}`, 'error');
            }
        }

        function sendMessage(message) {
            if (socket && socket.readyState === WebSocket.OPEN) {
                socket.send(JSON.stringify(message));
            }
        }

        function sendAudioData(audioData) {
            if (socket && socket.readyState === WebSocket.OPEN) {
                const pcmData = new Int16Array(audioData.length);
                for (let i = 0; i < audioData.length; i++) {
                    pcmData[i] = Math.max(-32768, Math.min(32767, audioData[i] * 32768));
                }
                socket.send(pcmData.buffer);
            }
        }

        function handleMessage(data) {
            switch (data.type) {
                case 'session_started':
                    log(`会话开始: ${data.session_id}`, 'info');
                    if (data.config) {
                        log(`服务器配置: 块大小=${data.config.chunk_size}, VAD块=${data.config.vad_chunk_size}ms`, 'info');
                    }
                    break;
                    
                case 'transcription':
                    handleTranscription(data);
                    break;
                    
                case 'final_result':
                    handleFinalResult(data);
                    break;
                    
                case 'vad':
                    if (data.segments && data.segments.length > 0) {
                        const segments = data.segments.map(seg => {
                            if (seg[0] === -1) return `结束${seg[1]}ms`;
                            if (seg[1] === -1) return `开始${seg[0]}ms`;
                            return `${seg[0]}-${seg[1]}ms`;
                        }).join(', ');
                        log(`VAD: ${segments}`, 'info');
                    }
                    break;
                    
                case 'error':
                    log(`服务器错误: ${data.message}`, 'error');
                    break;
                    
                default:
                    log(`未知消息: ${data.type}`, 'warning');
            }
        }

        function handleTranscription(data) {
            const realTimeResults = document.getElementById('realTimeResults');
            const completeTextDiv = document.getElementById('completeText');
            const timestamp = new Date().toLocaleTimeString();
            
            // 更新统计
            totalChunks = data.chunk_count || totalChunks;
            totalSentences = data.sentence_count || totalSentences;
            updateStats(totalChunks, totalSentences, data.confidence);
            
            // 显示实时结果
            const chunkDiv = document.createElement('div');
            chunkDiv.className = 'chunk-text';
            
            // 根据是否为新句子使用不同的样式
            const borderColor = data.is_new_sentence ? '#e74c3c' : '#007bff';
            chunkDiv.style.borderLeftColor = borderColor;
            
            chunkDiv.innerHTML = `
                <small style="color: #666;">[${timestamp}]</small> 
                <strong>${data.text}</strong>
                ${data.is_new_sentence ? ' <span style="color: #e74c3c;">🆕新句</span>' : ''}
                ${data.is_final ? ' <span style="color: #10b981;">✅完成</span>' : ''}
                ${data.has_punctuation ? ' <span style="color: #f39c12;">📝标点</span>' : ''}
                <small style="color: #999;"> (${(data.confidence * 100).toFixed(1)}%)</small>
            `;
            
            realTimeResults.appendChild(chunkDiv);
            
            // 保持最新的条目可见，但不要太频繁滚动
            if (realTimeResults.children.length % 3 === 0) {
                realTimeResults.scrollTop = realTimeResults.scrollHeight;
            }
            
            // 更新完整文本
            if (data.complete_text) {
                // 改进完整文本显示
                const formattedText = data.complete_text
                    .replace(/([。！？])\s*/g, '$1\n')  // 句号后换行
                    .replace(/([.!?])\s*/g, '$1\n')    // 英文标点后换行
                    .trim();
                
                completeTextDiv.innerHTML = formattedText.split('\n')
                    .map(sentence => sentence.trim())
                    .filter(sentence => sentence.length > 0)
                    .map(sentence => `<p style="margin: 8px 0; line-height: 1.6;">${sentence}</p>`)
                    .join('');
                
                document.getElementById('charCount').textContent = data.complete_text.length;
                
                if (data.has_punctuation) {
                    document.getElementById('punctuationStatus').textContent = '已处理';
                }
            }
            
            log(`转录: ${data.text} ${data.is_new_sentence ? '[新句]' : ''}${data.is_final ? '[完成]' : ''}`, 'success');
        }

        function handleFinalResult(data) {
            const completeTextDiv = document.getElementById('completeText');
            
            // 格式化最终完整文本
            const formattedText = data.complete_text
                .replace(/([。！？])\s*/g, '$1\n')
                .replace(/([.!?])\s*/g, '$1\n')
                .trim();
            
            completeTextDiv.innerHTML = formattedText.split('\n')
                .map(sentence => sentence.trim())
                .filter(sentence => sentence.length > 0)
                .map((sentence, index) => 
                    `<p style="margin: 8px 0; line-height: 1.6; padding: 8px; background: #f8f9fa; border-radius: 4px;">
                        <small style="color: #6c757d;">句${index + 1}:</small> ${sentence}
                    </p>`
                )
                .join('');
            
            document.getElementById('charCount').textContent = data.complete_text.length;
            document.getElementById('punctuationStatus').textContent = '已完成';
            
            // 更新统计
            totalChunks = data.total_chunks;
            totalSentences = data.total_sentences;
            updateStats(totalChunks, totalSentences);
            
            // 高亮显示最终结果
            completeTextDiv.style.border = '2px solid #28a745';
            setTimeout(() => {
                completeTextDiv.style.border = '1px solid #dee2e6';
            }, 2000);
            
            log(`🎉 最终结果: 共${data.total_chunks}块, ${data.total_sentences}句, ${data.complete_text.length}字符`, 'success');
        }

        // 页面加载时自动检查浏览器
        window.onload = () => {
            testBrowser();
        };
    </script>
</body>
</html> 